home *** CD-ROM | disk | FTP | other *** search
- PROGRAMMER'S NOTES
- An Explanation of How it All Works
- For Begginers
-
- Table Of Contents
- Purpose
- Audience
- Disclaimer
- Graphics Basics (Pixels and Bytes)
- Assembler - Basics
- The Virtual Screen
- Assembler - Drawing Tiles
- The Vertical Retrace
- Assembler - Showing a Virtual Screen
- Interlude: Optimizing Assembler - Speed Speed Speed
- Assembler - Drawing Sprites
- How I Got These Pictures
- Why Pascal
- Why I Wrote This
-
- Purpose
- =======
- This is a small tutorial for those not familiar with basic game
- graphics. It shows how tiles/sprites/little pictures can be drawn
- using a little bit of assembler language.
-
- Audience
- ========
- Many experienced programmers probably already know these things, so
- this is really targeted to people who aren't exactly sure how some
- of these things work.
-
- Disclaimer
- ==========
- Hey, I'm no genius, so some of this might be wrong and it is
- most certainly inefficient. But I do know it works. The sample
- code that comes with this was written in one afternoon (actually, a
- programmer afternoon, which is 6pm to 2am) and was debugged in another.
- I'm going to improve it eventually, just like I'll eventually add
- comments (I don't feel too bad as I comment a bit more than most
- programmers). Hopefully there's enough substance here to help a few
- people out.
-
- Graphics Basics (Pixels, Bytes and Mode 13h)
- ============================================
- The video screen you're staring at is made of of thousands of tiny
- pixels. A pixel is a small dot, the smallest element a program
- can use. It will always be one color. But which color? That gets
- set by the program running. How many choices does it have? That
- depends on how many bits are assigned to a pixel. A bit is
- either the number 0 or 1. On IBMs, bits are grouped in packs
- of 8 called bytes (the smallest piece of information a computer
- can directly affect). For video modes like text mode (your normal
- black and white screen), you can store 8 pixels in a byte, and
- since each pixel only gets, therefore, 1 bit, it only has the
- choice of two colors (0 or 1, often white or black or on older
- monitors dark green or light green). How do you change an
- individual pixel if your program can only address bytes -
- don't ask, you don't want to know (it involves masking, logical
- operators or just plain hard coding the image). The old CGA
- monitors had pixels that had 2 bits (4 colors), EGA had
- 4 bits (2 to the 4rth power, or 16 colors) and VGA has
- 8 bits (256 colors).
-
- The IBM PCs, by nature, have planar video modes. Now that
- you've heard the term, forget it - you wouldn't like it. It
- involves splitting the bits of a pixel over several bytes,
- so to get a color of one pixel that's 4 bits deep, you might
- have to obtain the 7th bit of bytes 1,2,3 and 4 in the video
- memory. Yuch. (There are advantages to this, but we'll just
- ignore them here).
-
- The PC does offer one video mode which is linear - mode 13h
- (that's 13 hex, or 19 in human numbers). It creates a screen
- that is 320x200 and each pixels has 8 bits, or 256 colors.
- Nicely enough, 8 bits is also the size of a full byte, so we can
- modify the colors without nasty masking and such.
-
- Since mode 13h is linear, it means the pixel next to you also
- happens to be the bit (or in this case, byte) in the video
- memory next to you. The mode 13h screen is essentially a large
- array (320x200 = 64,000) of bytes stored at the place in memory
- your computer reserves for the interface to your graphics card
- (this is 0A000:0000h for those curios - it's a different
- address for other video modes, especially normal and
- monochrome text modes which are at b800:0000 and b000:0000).
- So the screen is really just
- Screen = array [1..64000] of byte
-
- If you wanted, and many have done this, for a basic way to change
- the colors of pixels, you could declare an array at that offset
- (Screen = array [1..64000] of byte absolute $a000) and just change
- the contents directly (Screen [y*320+x]:=0; which is black).
- (Note: while this is great in a sloppy language like C, in Pascal,
- you cannot declare an array like the above because of range
- checking considerations - the real code is Screen = array[0..0] of byte
- and the PutPixel function is {$R-}Screen[y*320+x]:=color{$R+} ).
-
- In case you noticed, to get to the proper place in the array, you
- multiply the y coordinate by the length of the row (320 here) and
- add the x coordinate, so the coordinates 1,2 is Screen[641].
-
-
- In case your wondering, mode X, which seems popular now (for good
- reason), is a planar video mode, and much more complex to program. I
- won't cover it here.
-
- Assembler - Basics
- ==================
- The heart of all assembler routines is the movs function - move string.
- It comes in many variants - movsb, movsw and movsd (for byte, word -two
- bytes-, or double word -four bytes-). So in our situation, this
- will draw 1 pixel, 2 pixels or four pixels in one fell swoop.
-
- How does movs know where to get the data to copy and where to copy
- to? It uses certain built-in assembler variables - es,di,ds and si.
- Each of these is a word (2 bytes or 16 bits). The place to copy from
- is ds:si and the place to copy to is es:di (the : means put them
- together - it's segment offset format, a dos segmented memory
- model that plenty of people complain about).
-
- And how do figure out what numbers to put in there? Well, in
- assembler, the mov command is like the pascal := or the C =
- but that only helps so much because es and ds (extra segment
- and data segment) are wierd - you can't just copy any number
- into these places, they can only have values moved into them
- with mov from an assembler register (seen below). So you have
- another option - les and lds (load es and ds respectively).
- You can use these with your variables. So say you have
- an picture stored, byte for byte, in something like
- Picture = array [1..32,1..32] of byte; Fred : Picture.
- You would say copy from my picture by lds si,Picture
- which will get the segment and offset of wherever it
- is the program places your picture in memory and put it
- in ds:si.
-
- Assembler offers 4 basic registers - ax,bx,cx, and dx. Each is
- a word long. Sometimes they have special purposes - for example,
- cx is used by many assembler commands as a counter. You can load
- anything into these with the mov command.
-
- So you wanna draw something, right? Let's copy one pixel from
- the top left corner of our picture (in format array[1..32,1..32] of
- byte) into the top left corner of the screen. We'd do:
- mov ax,a000h ; where the screen starts
- mov es,ax ; load it into es
- mov di,0 ; set the rest to 0 so it's a000:0000
-
- lds si,MyPicture ; get the coordinate of our picture
-
- movsb ; and copy our pixel
-
- Note: the movs command automatically increments the source and
- destination pointers. So after this call, es:di, which was
- a000:0000 is now a000:0001. A call to movsw would have added two
- and a call to movsd (for 386s only) would have added four
- (a000:0002 and a000:0004 respectively). So if you called movsb
- again it would copy the next pixel in your picture to the next spot on
- the screen.
-
- One more thing you should know - many compilers have limitations on
- the kind of assembler you can use inside it. For example, my copy of
- Turbo Pascal and Borland C only allow me to use 286 instructions, not
- 386 instructions. It's 386 instructions that allow things like movsd and
- eax,,ebx,etc (32-bit, or double-word, registers). And my compiler
- doesn't even use 286 instructions by default - I had to go to copiler
- options to even get that (it starts with 8086 instructions, the next
- step down).
-
- The Virtual Screen
- ==================
- It is often beneficial to have a "scratch" screen. In arcade games
- (which are also tile-based by the way) they typically draw in layers -
- 1)Draw Background 2)Draw monsters and heroes and objects 3)Draw things
- that obscure the hero (like walking behind a cliff or tree and covering
- the hero). You probably don't want to show all that as you perform it.
- It's easier to just take your time drawing on an offscreen buffer and
- copying it when it's done.
-
- So what's an offscreen buffer? Just like the screen, it's an array. And
- how big is it? Well, if you use it to draw your whole screen, it's going
- to be [1..64000] of byte in mode 13h. In the example program, only a
- small portion (the travel map) is drawn, so make it as big as you need
- to.
-
- Assembler - Drawing Tiles
- =========================
- Here's where you learn how to draw a picture in an offscreen buffer. It
- assumes you have a picture. I use a tile type that's 32 pixels x 32.
- It's stored, pixel by pixel, top left to bottom right, in an
- array[1..32,1..32] of byte. The screen here is 9 tiles by 7 tiles (you
- can figure the pixels yourself - 9*32 x 7*32). The procedure is as
- follows:
-
- Procedure PlaceTileInBuffer (PixelX,PixelY:word; var Pic:icon32); assembler;
- const
- WordLength = TileWidth div 2;
- Asm
- (* figure pixel offset in buffer *)
- mov ax,BufWidth (* get length of buffer - 9*32 *)
- mul PixelY (* drop down to the line you want *)
- (* This automatically multiples the *)
- (* Y-val by ax, which is the X-val *)
- add ax,PixelX (* gives (Y*width)+x *)
-
- (* preserve data segment pointer *)
- (* Other things used this before you got it and will use *)
- (* it again when you're done with it, so save the value *)
- (* by moving it someplace we won't hurt it or alternately *)
- (* you could save it with push dx and restore with pop dx *)
- mov dx,ds
-
- (* Copy to where? *)
- les di,buffer (* buffer is your offscreen buffer *)
- mov di,ax (* move to the pixel in the buffer *)
- (* you want to start at *)
-
- (* Copy from where? *)
- lds si,Pic (* load your picture *)
-
- (* Copy Data *)
- mov bx,TileHeight (* here, 32 pixels *)
- @@CopyRowLoop:
- mov cx,WordLength (* how many words long is the row? *)
- push di (* save offset *)
- rep movsw (* copy cx words to the buffer *)
- pop di (* restore offset *)
- add di,BufWidth (* go to next line *)
- dec bx (* finished that row already *)
- jnz @@CopyRowLoop (* if there are any more rows in bx *)
- (* go ahead and do this again *)
-
- (* OK, all done, so quit *)
- mov ds,dx (* restore data segment pointer *)
- End;
-
- Ok, so what's it do? Basically, the code is something like
- Figure where in the buffer to draw this
- Specify where you're copying to
- Specify where you're copying from
- For i:= 1 to Number Of Rows do
- Copy Row to Buffer
-
- We used WordLength (16 words, since it was 32 bytes) so we could copy
- using movsw, which is supposed to be quicker than movsb. If this had
- been 386-optimized code, we would have done mov cx,8 rep movsd. And if
- you haven't figured it out, rep is repeat, which says do the following
- commands cx number of times (after it's done cx will be equal to 0).
-
- Now why did we copy row by row? It's because we only want to copy to a
- certain place (we're only copying one tile to a whole screen full
- of tiles). Imagine
-
- -------------------------------
- | | | | | | |
- -------------------------------
- | | | | | | |
- -------------------------------
- | | |000 | | | |
- -------------------------------
- | | | | | | |
- -------------------------------
- | | | | | | |
- -------------------------------
-
- Ok, it's not drawn to scale, but you get the point - the lines are
- your virtual screen and the 000 is the picture you just copied. You
- figure the x,y coords here (if the array is linear, like [1..64000] then
- the first 320 bytes are the first row, so the beggining of the second
- row is 321 and the beggining of the third row is 641, and the third
- pixel on the second row is 643, or in other words, (Y*BufferWidth)+X).
- So if es:di is pointing to, say, 0000:0010, which is the beggining of
- that tile, then you'd save that number (push di), copy 32 pixels (which
- means es:di is now 0042), restore the beggining of the picture (pop di),
- and move to the next line (add BufferWidth, or 320, making it 0330). If
- we didn't do this, we'd end up copying a tile that's 1 pixel high and
- really really long (32*32, which actually would wrap for about 3 lines).
-
- And what, pray tell, is meant by
- @@CopyRowLoop:
- dec bx
- jnz @@CopyRowLoop
- Well, we're going to use bx to store the number of rows (32). BX is used
- by the jump commands (jmp,jnz,jz,je,jne,etc). Jnz is jump if not zero,
- and dec bx means decrease (subtract 1). The @@: is pascalish for local
- label, which means jump to here (yes, it's a goto statement).
-
- So we say bx=32, copy a row, subtract one from bx, if bx does not equal
- 0 (if we have any rows left) go back and do it again. Alternately,
- rather than saving the starting pixel, we could have just added
- BufWidth-TileWidth. Whichever you prefer.
-
- We could also have used a loop statement (loop @@CopyEtc:) but it checks
- cx, which we were using for something else (the number of bytes to copy
- in rep movsw).
-
- So is this beggining to make sense?
-
- Assembler - Showing a Virtual Screen
- ====================================
- So how do you show a virtual screen?
-
- Well, if it depends on the size of the buffer. If the buffer is not the
- same size as the screen (which ours isn't) you pretend the buffer is
- just a big tile and copy it to the screen. Remember, you load the screen
- by doing
- mov ax,0a000h
- mov es,ax
- xor di,di ; this says di=0, which is faster than mov di,0
-
- And if it's the same size as the screen? Even better. Then we don't have
- to do any of this complicated adjusting for the next row stuff. It's
- just:
- push ds (* save the data segment *)
-
- lds si,Buffer; (* load scratch page *)
- mov ax,Screen_Offset; (* load screen coords *)
- mov es,ax (* es = 0a000h *)
- xor di,di (* di = 0 *)
- mov cx,32000 (* copy 32,000 words / 64,000 bytes *)
- rep movsw (* and do the copy *)
-
- pop ds (* restore the data segment *)
-
- The Vertical Retrace
- ====================
- So you almost have what you need - except that matter about the vertical
- retrace. The way a monitor works, a small beam inside the monitor shoots
- at the screen (the pixels/dot pitch, which are small pieces of phosphor)
- going from left to right, top to bottom (except on interlaced monitors
- which skips every other line and comes back for them later).
-
- When the beam strikes the phosphorus, it sets it to the color it has in
- it's memory. So what happens when you get unlucky and start drawing when
- the screen is halfway through the vertical retrace (that is, halfway
- through redrawing)? You draw only half of your picture until the next
- pass through. Which looks dorky. So what you need to do is wait until
- the vertical retrace is done, then copy your data to the screen -
- quickly. If you monitor refreshes 70 times a second, you have 1/70th of
- a second to copy your data in. That's why it's nice for the copy to
- screen routine to be fast (and why we use virtual screens to put things
- together before we finally blit them to the screen).
-
- And how do we do it?
- (* Wait for Vertical Retrace *)
- cli
- mov dx,3DAh
- @@label1:
- in al,dx
- and al,08h
- jnz @@label1
- @@label2:
- in al,dx
- and al,08h
- jz @@label2
- sti
- (* End Check for Retrace *)
- How's it work? You don't want to know - trust me. It turns off certain
- interrupt handlers then makes calls to the VGA ports (3DAh) and tests
- the results. All you need to know is that it works. This code is
- available from many ftp sites and numerous books (like the Programmer's
- Guide to the EGA/VGA, very good if you're the type who likes reading
- dictionaries).
-
- Interlude: Optimizing Assembler - Speed Speed Speed
- ===================================================
- Ok, a few assembler rules. You want as few lines as possible, although
- it's important to keep in mind that some instructions are slower than
- others. For example xor ax,ax is slower than mov ax,0 (don't know what
- and, or and xor do? They're logical operators. Given two conditions, say
- It Is Raining and My Dog Is White, and returns true (1) if both are
- true, or returns true if either is true, and xor returns true if only
- one is true. So for us, if you ANDed the number 101 and 011, the answer is
- 001 - only the last digit is true (1) in each number. If it had been
- ORed, the number would have been 111, and had it been XORed, the answer
- would have been 110. Any number XORed to itself is always all false,
- that is 00000000).
-
- So where can we save some room? Movsw is faster than movsb, and movsd is
- faster than movsw. That'll be important in the next section, as when we
- copy sprites (anything with a transparent background) we can only use
- movsb.
-
- You can also try unrolling your loops. If you're copying two rows,
- mov bx,TileHeight
- @@CopyRowLoop:
- mov cx,WordLength
- push di
- rep movsw
- pop di
- add di,BufWidth
- dec bx
- jnz @@CopyRowLoop
- is slower than
- (* copy one row *)
- mov cx,WordLength
- push di
- rep movsw
- pop di
- add di,BufWidth
- (* copy second row *)
- mov cx,WordLength
- push di
- rep movsw
- pop di
- add di,BufWidth
- That's because jump and loop statements (like jnz) are slow. In the
- latter example you're doing the same basic work without three of the
- lines (mov bx,TileHeight dec bx jnz @@CopyRowLoop), which means three
- less lines to execute.
-
- So what's the catch (yes, there is one) - this isn't very portable,
- which is a standard rule of assembler : specific routines are faster
- than generic ones. If you know your icons are 32x32 and your buffer is
- 320x200, you can code
- mov cx,16
- rep movsw
- add di,288
- (repeat 31 times more)
- The problem is, short of long, ugly code, is that this routine is no
- good for icons 16x16. For that, you'd write another routine (same code
- but different magic numbers - mov cx,8 add di,304). But if you're
- willing to do that, you can speed things up a bit. But be forewarned -
- if you buy or use a library from someone else (say the BGI functions
- that come with Borland products), chances are their blitting functions
- (functions to copy rectangular areas like sprites) are slower because of
- all the extra overhead. And lord forbid if those functions serve more
- than one video mode (like EGA and VGA).
-
- There are other places where code can be optimized, where certain
- statements are faster than others (on certain computers) but I don't
- know enough about assembler, frankly, to tell you what they are.
-
- Assembler - Drawing Sprites
- ===========================
- Sprites are like tiles, except they can do something nifty - they don't
- look like squares. Why? Because in our code, we specify a color that we
- will not draw. Examine the following:
- const
- WordLength = TileWidth div 2;
- Asm
- (* figure pixel offset in buffer *)
- mov ax,BufWidth (* we've seen this before, right? *)
- mul PixelY
- add ax,PixelX (* gives (Y*width)+x *)
-
- (* preserve data segment pointer *)
- push es (* I'm not sure es is so important *)
- push ds (* but this sure is *)
-
- (* Copy to where? *)
- les di,buffer (* point to our virtual screen *)
- mov di,ax
-
- (* Copy from where? *)
- lds si,Pic (* point to our sprite *)
-
- (* Copy Data - Don't draw black (color 0) pixels *)
- mov bx,TileHeight
- @@CopyRowLoop:
- mov cx,TileWidth (* how many words long is the row? *)
- push di (* save offset *)
- @@PutPixel:
- mov ax,[ds:si] (* retrieve value in ds:si *)
- cmp ax,0 (* is this a black (#0) pixel? *)
- je @@SkipPixel (* if so, skip it (goto SkipPixel *)
- movsb (* copy cx words to the buffer *)
- loop @@PutPixel (* keep looping until cx=0 *)
-
- (* Move to Next Row *)
- @@EndOfRow:
- pop di (* restore offset *)
- add di,BufWidth (* go to next line *)
- dec bx (* finished that row already *)
- jnz @@CopyRowLoop (* if there are any more rows in bx *)
- (* go ahead and do this again *)
- jmp @@Done
-
- @@SkipPixel:
- inc di (* if we don't do movsb then we must *)
- inc si (* manually increase si and di *)
- dec cx (* and decrease the counter *)
- cmp cx,0 (* are we at the end of the row? *)
- je @@EndOfRow (* if so, go to end of row line *)
- jmp @@PutPixel (* otherwise, do the next pixel *)
-
- (* OK, all done, so quit *)
- @@Done:
- pop ds (* restore data segment pointer *)
- pop es
- End;
-
- And what does it mean? Basically, it's
- for y:=1 to number of rows do
- for x:=1 to number of pixels in a row do
- if the pixel is not black
- movsb
- else
- manually update the pointers
- (ie., move over to next pixel)
- In here
- @@PutPixel:
- mov ax,[ds:si] (* retrieve value in ds:si *)
- cmp ax,0 (* is this a black (#0) pixel? *)
- je @@SkipPixel (* if so, skip it (goto SkipPixel *)
- movsb (* copy cx words to the buffer *)
- loop @@PutPixel (* keep looping until cx=0 *)
- we're reading the value pointed to by ds:si (ds:si is a number like
- 0000:0123 while [ds:si] is a byte like the number 4 or 256) and then
- comparing that to 0 (the color we don't want to draw). If it's not the
- unwanted color, then do the old familiar movsb (if we did movsw, we'd
- have copied the next pixel too, whether it was the unwanted color or
- not). And then we go back to the PutPixel loop until we've finished
- copying all the data for that row (ie., when cx=0).
-
- And if ax does hold the color we don't want?
- @@SkipPixel:
- inc di (* if we don't do movsb then we must *)
- inc si (* manually increase si and di *)
- dec cx (* and decrease the counter *)
- cmp cx,0 (* are we at the end of the row? *)
- je @@EndOfRow (* if so, go to end of row line *)
- jmp @@PutPixel (* otherwise, do the next pixel *)
- Movsb moves over to the next pixel for us and decreases the counter
- (cx). If we don't use movsb (we don't draw the pixel) then we have to
- manually update these. We also need to check cx to see if we just drew
- (or in this case skipped) the last pixel in that row. If not, we go to
- the next pixel, if so, we do the end of row routines like normal (move
- to the next line and reset the counter).
-
- Needless to say, given all the tests and jumping around we do, this is
- not the fastest routine there is.
-
- How I Got These Pictures
- ========================
- Well, that's about all there is to it to the tiny tutorial on tile-based
- graphics. I hope my spelling held out so far (it normally doesn't and
- I'm not going to spend the time editing this, so forgive any nonsense
- sentences).
-
- But how did I get those pictures? The old question of sprite editors and
- the like. I used a really convulted way - draw the pictures in Deluxe
- Animator (my drawing program of choice) and then writing a small program
- that displays the reulting pcx file created by DA and moving a 32x32
- rectangle down to the picture I want. I then press enter and it saves
- that 32x32 region to disk, which I promptly rename. I will eventually
- write another sprite editor (I've written several already, mostly for
- ega and text font editors) but for now this works for me. Sorry this
- doesn't help to many of you out.
-
- The icons are easy - just layer colors next to each other (like the
- water which is light blue with a dark blue undercoating which helps set
- it off). I've seen columns done in arcade games with a solid white line
- in the middle and just small, darker lines on either side (sounds silly
- but it actually looks ok). The grass was a solid green background with
- DA's spray paint tool used to put light green and black on top of it.
-
- I hear some people make 256-color sprite editors, though I never saw one
- I liked. Many people use Autodesk Animator (which I don't own
- regretably but hear is nice). Others (like Sierra) just paint pictures
- and scan them in. If you get a drawing tool, what I do to cheat is to
- place a piece of see-through paper on the screen with a drawing I did by
- hand and then try to trace that with the mouse on the screen.
-
- But writing a tile-editor is nice, because eventually you'll have to
- write a map editor (if no one's seen one, I have one hardcoded for a
- game I wrote once I could show around, but it let's you draw maps by
- picking tiles the way you normally pick colors and drawing those
- around with PutTile commands like those we did here).
-
- Why Pascal
- ==========
- This demo was written in pascal because I didn't want to waste time with
- stupid errors that C seems to like so much. The code is just as fast or
- faster in turbo pascal than C, and the executable's smaller because of
- the way TPUs are linked in versus C libraries.
-
- But the truth is either is pretty much the same. They use almost the
- same commands and nothing in this demo is so wierd that couldn't be
- translated, line for line, to C. For example, the hard stuff (assembler)
- was Asm stuff... End; in pascal and is asm { stuff } in C. Not so bad.
- Given that I'll probably rewrite parts of this in C++ (or at least a
- game using many of these constructs) I'm glad it'll port nicely. Truth
- is, if you know a language and want a quick game, write with what you
- know. Last I heard, the fastest compiler was a basic compiler anyway
- (made by some European or Australian company - heard the documentation is
- pretty bad though). Just do what feels good - the fast parts are in
- assembler and the rest doesn't hurt your performance no matter what
- langauge you use. Cobol with these routines would make a nice fast game.
-
- Why I Wrote This
- ================
- Three years ago (Spring 1991) my roommate, seeing I was depressed with
- my major (journalism), talked me into using a computer, signing up for
- beggining programming. 3 years wasn't that long ago, so I remember quite
- clearly feeling like maybe I was a bright guy and still had no clue how
- to do what I wanted. Computers frustrated me at every turn.
-
- Writing computer games isn't a matter of intelligence, it's a matter of
- experience. If you're a bright person, there's no reason why you
- shouldn't have some help learning the basics, and no one should make you
- feel dumb (as I often did) for not knowing.
-
- I wish I had something like this to help me rather than all those
- (expensive) books I ended up buying that only briefly touched on what I
- wanted to know. I am indebted to people like Josh Jensen, Themie, Vic
- Putz, Dr Cat and others who helped me out when I was completely lost. I
- certainly wouldn't know anything if it hadn't been for other people's
- help. Hopefully I can help pay them back by passing on what meager
- amount I know.
-
- And by the way, I never did changes majors to computer science. It took
- me 5 semesters to pass calculus, at which point I figured I'd cop out
- and get a business degree. And now I wear a tie. Friends don't let
- friends wear ties.
-
- - baylor
-